Skip to content

fix(pool): prune stale worktree registrations before add in Acquire#31

Open
e-jung wants to merge 2 commits into
kunchenguid:mainfrom
e-jung:fix/get-prunes-stale-worktree-registrations
Open

fix(pool): prune stale worktree registrations before add in Acquire#31
e-jung wants to merge 2 commits into
kunchenguid:mainfrom
e-jung:fix/get-prunes-stale-worktree-registrations

Conversation

@e-jung

@e-jung e-jung commented Jun 20, 2026

Copy link
Copy Markdown

What Changed

  • acquire now runs git worktree prune before adding a worktree, so a stale registration left behind by a crashed or forcibly removed worktree no longer wedges get with a "missing but already registered worktree" error.
  • Added the git.PruneWorktrees helper (internal/git/git.go) that shells out to git worktree prune, which only removes registrations for already-missing directories.
  • Added the TestGetRecoversFromStaleWorktreeRegistration e2e regression test and documented get's self-healing behavior in README.md and AGENTS.md.

Risk Assessment

✅ Low: The change is a small, well-bounded, self-healing fix: a single safe git worktree prune call (only removes registrations for already-missing directories) placed before git worktree add in the create-new path, serialized under the existing flock, with a well-constructed e2e test.

Testing

Confirmed the self-healing intent end-to-end: with a prunable stale worktree registration planted at the slot path, the pre-fix binary's get wedges with "missing but already registered worktree" (exit 1, dir not recreated), while the fixed binary's get prunes it and successfully creates the worktree (exit 0, README.md recreated, returned to pool). The accompanying unit/e2e test passes on the fix and fails on the base commit, and the entire go test ./... suite is green. No UI/rendered surface applies to this CLI change; evidence is captured as CLI transcripts.

Evidence: Combined before/after CLI demo (pre-fix fails, fixed recovers)

PRE-FIX (base 68fa3d2): === treehouse get === exit code: 1 failed to create worktree: git worktree add --detach .../myrepo-435da5/1/myrepo main: fatal: '...' is a missing but already registered worktree; use 'add -f' to override, or 'prune' or 'remove' to clear --- worktree dir recreated? --- NO: .../1/myrepo/README.md MISSING FIXED (target 0ed4610): === treehouse get === exit code: 0 Setting up worktree... Entered worktree at ~/.treehouse/myrepo-a8be86/1/myrepo. Type 'exit' to return. Worktree returned to pool. --- git worktree list AFTER get --- (prunable entry is gone) --- worktree dir recreated? --- YES: .../1/myrepo/README.md exists

########## PRE-FIX (base commit 68fa3d2) ##########
===== BASE (/tmp/treehouse-base) =====
repo: /tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-BASE/myrepo
pool: /tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-BASE/home/.treehouse/myrepo-435da5
slot: /tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-BASE/home/.treehouse/myrepo-435da5/1/myrepo

--- git worktree list --porcelain BEFORE get (note: prunable) ---
worktree /tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-BASE/myrepo
HEAD bb178d631a5dcdad24fdd771070aad491edbb86b
branch refs/heads/main

worktree /tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-BASE/home/.treehouse/myrepo-435da5/1/myrepo
HEAD bb178d631a5dcdad24fdd771070aad491edbb86b
detached
prunable gitdir file points to non-existent location


=== treehouse get (SHELL=/bin/true) ===
exit code: 1
--- stderr ---
🌳 Setting up worktree...
failed to create worktree: git worktree add --detach /tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-BASE/home/.treehouse/myrepo-435da5/1/myrepo main: fatal: '/tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-BASE/home/.treehouse/myrepo-435da5/1/myrepo' is a missing but already registered worktree;
use 'add -f' to override, or 'prune' or 'remove' to clear

--- git worktree list --porcelain AFTER get ---
worktree /tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-BASE/myrepo
HEAD bb178d631a5dcdad24fdd771070aad491edbb86b
branch refs/heads/main

worktree /tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-BASE/home/.treehouse/myrepo-435da5/1/myrepo
HEAD bb178d631a5dcdad24fdd771070aad491edbb86b
detached
prunable gitdir file points to non-existent location


--- worktree dir recreated? ---
NO: /tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-BASE/home/.treehouse/myrepo-435da5/1/myrepo/README.md MISSING
===== end BASE =====

########## FIXED (target commit 0ed4610) ##########
===== FIXED (/tmp/treehouse-fixed) =====
repo: /tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-FIXED/myrepo
pool: /tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-FIXED/home/.treehouse/myrepo-a8be86
slot: /tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-FIXED/home/.treehouse/myrepo-a8be86/1/myrepo

--- git worktree list --porcelain BEFORE get (note: prunable) ---
worktree /tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-FIXED/myrepo
HEAD bb178d631a5dcdad24fdd771070aad491edbb86b
branch refs/heads/main

worktree /tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-FIXED/home/.treehouse/myrepo-a8be86/1/myrepo
HEAD bb178d631a5dcdad24fdd771070aad491edbb86b
detached
prunable gitdir file points to non-existent location


=== treehouse get (SHELL=/bin/true) ===
exit code: 0
--- stderr ---
🌳 Setting up worktree...
🌳 Entered worktree at ~/.treehouse/myrepo-a8be86/1/myrepo. Type 'exit' to return.
🌳 Worktree returned to pool.

--- git worktree list --porcelain AFTER get ---
worktree /tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-FIXED/myrepo
HEAD bb178d631a5dcdad24fdd771070aad491edbb86b
branch refs/heads/main

worktree /tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-FIXED/home/.treehouse/myrepo-a8be86/1/myrepo
HEAD bb178d631a5dcdad24fdd771070aad491edbb86b
detached


--- worktree dir recreated? ---
YES: /tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-FIXED/home/.treehouse/myrepo-a8be86/1/myrepo/README.md exists
===== end FIXED =====
Evidence: New e2e test run against BASE source proves it catches the regression

Source: New e2e test run against BASE source proves it catches the regression (local file: /tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ)

=== RUN TestGetRecoversFromStaleWorktreeRegistration
e2e_test.go:722: treehouse get failed on stale registration (code 1): failed to create worktree: ... fatal: '...' is a missing but already registered worktree;
--- FAIL: TestGetRecoversFromStaleWorktreeRegistration (0.13s)
Evidence: Reproducible demo script (sets up repo, plants stale registration, runs get)
#!/usr/bin/env bash
# End-to-end demo: treehouse get self-heals from a stale worktree registration.
set -u

BIN="$1"
LABEL="$2"

DEMO_BASE="/tmp/no-mistakes-evidence/01KW9VEPA2WGD9GH7HH0HJ7AQZ/run-${LABEL}"
rm -rf "$DEMO_BASE"
mkdir -p "$DEMO_BASE"

HOME_DIR="$DEMO_BASE/home"
REPO_DIR="$DEMO_BASE/myrepo"
mkdir -p "$HOME_DIR"

git init --initial-branch=main "$REPO_DIR" >/dev/null 2>&1
git -C "$REPO_DIR" config user.email "demo@test.com"
git -C "$REPO_DIR" config user.name "Demo"
echo "# Demo Project" > "$REPO_DIR/README.md"
git -C "$REPO_DIR" add . >/dev/null
git -C "$REPO_DIR" commit -m "initial commit" >/dev/null 2>&1

export HOME="$HOME_DIR"
export TREEHOUSE_NO_UPDATE_CHECK=1

echo "===== $LABEL ($BIN) ====="

( cd "$REPO_DIR" && "$BIN" status ) >/dev/null 2>&1

REPO_NAME="$(basename "$REPO_DIR")"
POOL_DIR="$(ls -d "$HOME_DIR/.treehouse/${REPO_NAME}-"* 2>/dev/null || true)"
SLOT_PATH="$POOL_DIR/1/$REPO_NAME"

if [ -z "$POOL_DIR" ]; then
  echo "ERROR: pool dir not materialized for $REPO_NAME"
  ls -la "$HOME_DIR/.treehouse/" 2>&1
  exit 1
fi

echo "repo: $REPO_DIR"
echo "pool: $POOL_DIR"
echo "slot: $SLOT_PATH"

mkdir -p "$(dirname "$SLOT_PATH")"
git -C "$REPO_DIR" worktree add --detach "$SLOT_PATH" main >/dev/null 2>&1
rm -rf "$SLOT_PATH"

echo ""
echo "--- git worktree list --porcelain BEFORE get (note: prunable) ---"
git -C "$REPO_DIR" worktree list --porcelain

echo ""
echo "=== treehouse get (SHELL=/bin/true) ==="
( cd "$REPO_DIR" && SHELL=/bin/true "$BIN" get ) 2>"$DEMO_BASE/get.stderr"
GET_CODE=$?
echo "exit code: $GET_CODE"
echo "--- stderr ---"
cat "$DEMO_BASE/get.stderr"

echo ""
echo "--- git worktree list --porcelain AFTER get ---"
git -C "$REPO_DIR" worktree list --porcelain

echo ""
echo "--- worktree dir recreated? ---"
if [ -f "$SLOT_PATH/README.md" ]; then
  echo "YES: $SLOT_PATH/README.md exists"
else
  echo "NO: $SLOT_PATH/README.md MISSING"
fi
echo "===== end $LABEL ====="

Pipeline

Updates from git push no-mistakes

⏭️ **intent** - skipped

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • go test ./cmd/ -run TestGetRecoversFromStaleWorktreeRegistration -v (passes on target commit)
  • go test ./... (full suite, all packages pass)
  • Ran the new e2e test against the base commit source (68fa3d2) — FAILS with the exact bug, confirming it is a valid regression test
  • Manual end-to-end CLI reproduction with /tmp/treehouse-base (pre-fix): simulated a crashed scout by registering a worktree at Acquire's slot path then deleting its dir; treehouse get failed with 'fatal: ... is a missing but already registered worktree' (exit 1)
  • Manual end-to-end CLI reproduction with /tmp/treehouse-fixed (this change): same scenario; treehouse get succeeded (exit 0), pruned the prunable entry, recreated the worktree dir with README.md, and printed 'Entered worktree at ...' + 'Worktree returned to pool.'
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

e-jung added 2 commits June 29, 2026 14:10
A crashed or forcibly removed worktree leaves prunable bookkeeping in
.git/worktrees/. The next "treehouse get" then fails hard with
"missing but already registered worktree" because git refuses the add,
wedging every subsequent spawn until a human runs "git worktree prune"
by hand.

Run "git worktree prune" immediately before "git worktree add" in
Acquire's create-new-worktree branch. Prune is safe by design: it only
removes registrations whose target directories are already gone, so it
cannot destroy live worktrees or data.

Adds TestGetRecoversFromStaleWorktreeRegistration covering the recovery.

Refs kunchenguid#30
@e-jung e-jung force-pushed the fix/get-prunes-stale-worktree-registrations branch from 2835921 to 0ed4610 Compare June 29, 2026 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant